// NOTE: The contents of this file are all exported from the namespace 'documents'. This is to
//       support the eventual conversion of harness into a modular system.

namespace documents {
    export class TextDocument {
        public readonly meta: Map<string, string>;
        public readonly file: string;
        public readonly text: string;

        private _lineStarts: readonly number[] | undefined;
        private _testFile: Harness.Compiler.TestFile | undefined;

        constructor(file: string, text: string, meta?: Map<string, string>) {
            this.file = file;
            this.text = text;
            this.meta = meta || new Map<string, string>();
        }

        public get lineStarts(): readonly number[] {
            return this._lineStarts || (this._lineStarts = ts.computeLineStarts(this.text));
        }

        public static fromTestFile(file: Harness.Compiler.TestFile) {
            return new TextDocument(
                file.unitName,
                file.content,
                file.fileOptions && Object.keys(file.fileOptions)
                    .reduce((meta, key) => meta.set(key, file.fileOptions[key]), new Map<string, string>()));
        }

        public asTestFile() {
            return this._testFile || (this._testFile = {
                unitName: this.file,
                content: this.text,
                fileOptions: Array.from(this.meta)
                    .reduce((obj, [key, value]) => (obj[key] = value, obj), {} as Record<string, string>)
            });
        }
    }

    export interface RawSourceMap {
        version: number;
        file: string;
        sourceRoot?: string;
        sources: string[];
        sourcesContent?: string[];
        names: string[];
        mappings: string;
    }

    export interface Mapping {
        mappingIndex: number;
        emittedLine: number;
        emittedColumn: number;
        sourceIndex: number;
        sourceLine: number;
        sourceColumn: number;
        nameIndex?: number;
    }

    export class SourceMap {
        public readonly raw: RawSourceMap;
        public readonly mapFile: string | undefined;
        public readonly version: number;
        public readonly file: string;
        public readonly sourceRoot: string | undefined;
        public readonly sources: readonly string[] = [];
        public readonly sourcesContent: readonly string[] | undefined;
        public readonly mappings: readonly Mapping[] = [];
        public readonly names: readonly string[] | undefined;

        private static readonly _mappingRegExp = /([A-Za-z0-9+/]+),?|(;)|./g;
        private static readonly _sourceMappingURLRegExp = /^\/\/[#@]\s*sourceMappingURL\s*=\s*(.*?)\s*$/mig;
        private static readonly _dataURLRegExp = /^data:application\/json;base64,([a-z0-9+/=]+)$/i;
        private static readonly _base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

        private _emittedLineMappings: Mapping[][] = [];
        private _sourceLineMappings: Mapping[][][] = [];

        constructor(mapFile: string | undefined, data: string | RawSourceMap) {
            this.raw = typeof data === "string" ? JSON.parse(data) as RawSourceMap : data;
            this.mapFile = mapFile;
            this.version = this.raw.version;
            this.file = this.raw.file;
            this.sourceRoot = this.raw.sourceRoot;
            this.sources = this.raw.sources;
            this.sourcesContent = this.raw.sourcesContent;
            this.names = this.raw.names;

            // populate mappings
            const mappings: Mapping[] = [];
            let emittedLine = 0;
            let emittedColumn = 0;
            let sourceIndex = 0;
            let sourceLine = 0;
            let sourceColumn = 0;
            let nameIndex = 0;
            let match: RegExpExecArray | null;
            while (match = SourceMap._mappingRegExp.exec(this.raw.mappings)) {
                if (match[1]) {
                    const segment = SourceMap._decodeVLQ(match[1]);
                    if (segment.length !== 1 && segment.length !== 4 && segment.length !== 5) {
                        throw new Error("Invalid VLQ");
                    }

                    emittedColumn += segment[0];
                    if (segment.length >= 4) {
                        sourceIndex += segment[1];
                        sourceLine += segment[2];
                        sourceColumn += segment[3];
                    }

                    const mapping: Mapping = { mappingIndex: mappings.length, emittedLine, emittedColumn, sourceIndex, sourceLine, sourceColumn };
                    if (segment.length === 5) {
                        nameIndex += segment[4];
                        mapping.nameIndex = nameIndex;
                    }

                    mappings.push(mapping);

                    const mappingsForEmittedLine = this._emittedLineMappings[mapping.emittedLine] || (this._emittedLineMappings[mapping.emittedLine] = []);
                    mappingsForEmittedLine.push(mapping);

                    const mappingsForSource = this._sourceLineMappings[mapping.sourceIndex] || (this._sourceLineMappings[mapping.sourceIndex] = []);
                    const mappingsForSourceLine = mappingsForSource[mapping.sourceLine] || (mappingsForSource[mapping.sourceLine] = []);
                    mappingsForSourceLine.push(mapping);
                }
                else if (match[2]) {
                    emittedLine++;
                    emittedColumn = 0;
                }
                else {
                    throw new Error(`Unrecognized character '${match[0]}'.`);
                }
            }

            this.mappings = mappings;
        }

        public static getUrl(text: string) {
            let match: RegExpExecArray | null;
            let lastMatch: RegExpExecArray | undefined;
            while (match = SourceMap._sourceMappingURLRegExp.exec(text)) {
                lastMatch = match;
            }
            return lastMatch ? lastMatch[1] : undefined;
        }

        public static fromUrl(url: string) {
            const match = SourceMap._dataURLRegExp.exec(url);
            return match ? new SourceMap(/*mapFile*/ undefined, ts.sys.base64decode!(match[1])) : undefined;
        }

        public static fromSource(text: string): SourceMap | undefined {
            const url = this.getUrl(text);
            return url === undefined ? undefined : this.fromUrl(url);
        }

        public getMappingsForEmittedLine(emittedLine: number): readonly Mapping[] | undefined {
            return this._emittedLineMappings[emittedLine];
        }

        public getMappingsForSourceLine(sourceIndex: number, sourceLine: number): readonly Mapping[] | undefined {
            const mappingsForSource = this._sourceLineMappings[sourceIndex];
            return mappingsForSource && mappingsForSource[sourceLine];
        }

        private static _decodeVLQ(text: string): number[] {
            const vlq: number[] = [];
            let shift = 0;
            let value = 0;
            for (let i = 0; i < text.length; i++) {
                const currentByte = SourceMap._base64Chars.indexOf(text.charAt(i));
                value += (currentByte & 31) << shift;
                if ((currentByte & 32) === 0) {
                    vlq.push(value & 1 ? -(value >>> 1) : value >>> 1);
                    shift = 0;
                    value = 0;
                }
                else {
                    shift += 5;
                }
            }
            return vlq;
        }
    }
}